﻿using System;
using System.Collections.Generic;
using System.Reflection;

using BizElements.Core;

namespace BizElements.BusinessLayer
{
    /// <summary>Contains information about entity structure and security, ie. acces permissions, and provides generic interface for children management.</summary>
    public class BusinessObjectDescriptor : DataObjectDescriptor, IBusinessObjectDescriptor
    {
        EntityPermissionSet permissions = new EntityPermissionSet();

        #region IBusinessObjectDescriptor.

        /// <summary>Definitions/IDs of commonly used entity functions/permissions such as: read, write, assignment, UI functions.</summary>
        public EntityPermissionSet Permissions
        {
            get { return this.permissions; }
            set
            {
                if (value == null)
                    throw new ArgumentNullException("value", "Null is not a valid value for Permissions property.");

                this.permissions = value;
            }
        }
        
        /// <summary>Gets or sets children collection adapters and meta owned by entity. Empty collection if class doesn't contain child entity collections.</summary>
        public IEnumerable<IChildrenAdapter> Children { get; set; }
        /// <summary>Gets or sets complex property adapters and meta owned by entity. Empty collection if class doesn't contain complex properties.</summary>
        public IEnumerable<IComplexPropertyAdapter> ComplexProperties { get; set; }

        /// <summary>Gets or set lock mode for pessimistic offline locking control scheme. <b>null</b> indicates that locking is not used.</summary>
        public LockMode? PessimisticLockMode { get; set; }

        #endregion

        #region Permissions - exposed EntityPermissions getters and setters.

        /// <summary>Gets or sets permission/action ID required to read/access existing entity.</summary>
        public object ReadPermission
        {
            get { return this.Permissions.ReadPermission; }
            set { this.Permissions.ReadPermission = value; }
        }

        /// <summary>Gets or sets permission/action ID required to read/access existing entity that belong to the user's business unit.</summary>
        public object ReadUnitPermission
        {
            get { return this.Permissions.ReadUnitPermission; }
            set { this.Permissions.ReadUnitPermission = value; }
        }

        /// <summary>Gets or sets permission/action ID required to read/access existing entity that belong to the user's business unit and its child units.</summary>
        public object ReadChildUnitPermission
        {
            get { return this.Permissions.ReadChildUnitPermission; }
            set { this.Permissions.ReadChildUnitPermission = value; }
        }

        /// <summary>Gets or sets permission/action ID required to read/access any existing entity that belongs to user's organization, ie. no row-level security is enforced.</summary>
        public object ReadAllPermission
        {
            get { return this.Permissions.ReadAllPermission; }
            set { this.Permissions.ReadAllPermission = value; }
        }

        /// <summary>Gets or sets permission/action ID required to read/access unassigned entities, ie. the ones where neither owner nor unit nor team have been set.</summary>
        public object ReadUnassignedPermission
        {
            get { return this.Permissions.ReadUnassignedPermission; }
            set { this.Permissions.ReadUnassignedPermission = value; }
        }


        /// <summary>Gets or sets permission/action ID required to create a new entity.</summary>
        public object CreatePermission
        {
            get { return this.Permissions.CreatePermission; }
            set { this.Permissions.CreatePermission = value; }
        }

        /// <summary>Gets or sets permission/action ID required to update existing entity.</summary>
        public object UpdatePermission
        {
            get { return this.Permissions.UpdatePermission; }
            set { this.Permissions.UpdatePermission = value; }
        }

        /// <summary>Gets or sets permission/action ID required to deactivate/delete existing entity.</summary>
        public object DeletePermission
        {
            get { return this.Permissions.DeletePermission; }
            set { this.Permissions.DeletePermission = value; }
        }


        /// <summary>Gets or sets permission/action ID required to (re)assign ownership of entity to another user from the user's organization.</summary>
        public object AssignOwnerPermission
        {
            get { return this.Permissions.AssignOwnerPermission; }
            set { this.Permissions.AssignOwnerPermission = value; }
        }

        /// <summary>Gets or sets permission/action ID required to (re)assign ownership of entity to a child business unit.</summary>
        public object AssignChildUnitPermission
        {
            get { return this.Permissions.AssignChildUnitPermission; }
            set { this.Permissions.AssignChildUnitPermission = value; }
        }

        /// <summary>Gets or sets permission/action ID required to (re)assign ownership of entity to any business unit in the user's organization.</summary>
        public object AssignAnyUnitPermission
        {
            get { return this.Permissions.AssignAnyUnitPermission; }
            set { this.Permissions.AssignAnyUnitPermission = value; }
        }

        /// <summary>Gets or sets permissio/action ID required to (re)assign ownership of entity to any team in the user's organization.</summary>
        public object AssignAnyTeamPermission
        {
            get { return this.Permissions.AssignAnyTeamPermission; }
            set { this.Permissions.AssignAnyTeamPermission = value; }
        }

        /// <summary>Gets or sets permission/action ID required to view and use entity primary key.</summary>
        public object ViewPkPermission
        {
            get { return this.Permissions.ViewPkPermission; }
            set { this.Permissions.ViewPkPermission = value; }
        }

        /// <summary>Gets or sets permission/action ID required to manage entity via application's user interface.</summary>
        public object UserInterfacePermission
        {
            get { return this.Permissions.UserInterfacePermission; }
            set { this.Permissions.UserInterfacePermission = value; }
        }

        #endregion

        #region TODO - CreateViaReflection.

        /// <summary>Attempts to create adapter through reflection. Assumes naming conventions as implememented in BizElements XML templates that generate business object classes and permissions.</summary>
        /// <param name="compositeEntityClass">Parent entity class that implements <see cref="IBusinessObject"/> interface.</param>
        /// <returns>Adapter for the specified class.</returns>
        private static BusinessObjectDescriptor CreateViaReflection(Type compositeEntityClass)
        {
            string bizobjClassName = compositeEntityClass.Name;
            IBusinessObject compositeEntity = (IBusinessObject)Activator.CreateInstance(compositeEntityClass);
            var descriptor = new BusinessObjectDescriptor() 
            {
                DefaultPropertyName = GetFirstTextualProperty(compositeEntity.Table),

                ReadPermission = string.Format("{0}_Read", bizobjClassName),
                ReadUnitPermission = string.Format("{0}_ReadUnit", bizobjClassName),
                ReadChildUnitPermission = string.Format("{0}_ReadChildUnit", bizobjClassName),
                ReadAllPermission = string.Format("{0}_ReadAll", bizobjClassName),
                ReadUnassignedPermission = string.Format("{0}_ReadUnassignedPermission", bizobjClassName),

                CreatePermission = string.Format("{0}_Create", bizobjClassName),                
                UpdatePermission = string.Format("{0}_Update", bizobjClassName),
                DeletePermission = string.Format("{0}_Deactivate", bizobjClassName),

                AssignOwnerPermission = string.Format("{0}_AssignOwnerPermission", bizobjClassName),
                AssignChildUnitPermission = string.Format("{0}_AssignChildUnitPermission", bizobjClassName),
                AssignAnyUnitPermission = string.Format("{0}_AssignAnyUnitPermission", bizobjClassName),
                AssignAnyTeamPermission = string.Format("{0}_AssignAnyTeamPermission", bizobjClassName),
                
                ViewPkPermission = string.Format("{0}_UI_ViewPk", bizobjClassName),
                UserInterfacePermission = string.Format("{0}_UI", bizobjClassName),
                
                FieldPermissions = new FieldPermissionSet[0]
            };

            descriptor.Children = CreateChildrenAdaptersViaReflection(compositeEntityClass, compositeEntity);
            descriptor.ComplexProperties = CreateComplexPropertyAdaptersViaReflection(compositeEntityClass);
            return descriptor;
        }       

        private static string GetFirstTextualProperty(IDbTable tbl)
        {
            foreach (IDbColumn col in tbl.Columns)
            {
                if (TypeUtil.IsText(col.DataType))
                    return col.PropertyName;
            }

            return null;
        }

        private static IEnumerable<PropertyInfo> GetEntityCollectionProperties(Type compositeEntityClass)
        {
            foreach (PropertyInfo prop in compositeEntityClass.GetProperties())
            {
                foreach (Type implementedInterface in prop.PropertyType.GetInterfaces())
                {
                    if (implementedInterface == typeof(IEntityCollection))
                        yield return prop;
                }
            }
        }

        private static List<IChildrenAdapter> CreateChildrenAdaptersViaReflection(Type compositeEntityClass, IBusinessObject compositeEntity)
        {
            var childCollections = GetEntityCollectionProperties(compositeEntityClass);
            var childrenAdapters = new List<IChildrenAdapter>();
            foreach (var childrenProperty in childCollections)
            {
                Type childBizObjectClass = GetChildBizObjectClass(compositeEntityClass, childrenProperty);
                DbRelation fk = GetChildForeingKey(childrenProperty.Name, childBizObjectClass, compositeEntity.Table.TableName);
                childrenAdapters.Add(new ReflectionChildrenAdapter(childrenProperty.Name, childBizObjectClass, fk));
            }
            return childrenAdapters;
        }

        private static Type GetChildBizObjectClass(Type compositeEntityClass, PropertyInfo childrenProperty)
        {
            IEntityCollection collection = (IEntityCollection)Activator.CreateInstance(childrenProperty.PropertyType);
            string childEntityClassName = collection.IDbTable.NewEntity().GetType().Name;
            string childBizObjectClassName = TextUtil.RemoveSuffix(childEntityClassName, "Entity");
            string bizLayerNamespace = compositeEntityClass.Namespace;
            Type childBizObjectClass = compositeEntityClass.Assembly.GetType(bizLayerNamespace + "." + childBizObjectClassName);
            return childBizObjectClass;
        }

        private static DbRelation GetChildForeingKey(string childrenPropertyName, Type childBizObjectClass, string parentTableName)
        {
            IEntity childInstance = (IEntity)Activator.CreateInstance(childBizObjectClass);
            var tbl = childInstance.Table;
            List<DbRelation> relsToCurrParent = new List<DbRelation>();
            foreach (DbRelation fk in tbl.ForeignKeys)
            {
                if (fk.Parent.TableName == parentTableName)
                    relsToCurrParent.Add(fk);
            }

            if (relsToCurrParent.Count == 1)
                return relsToCurrParent[0];

            char childOrdinalSuffix = childrenPropertyName[childrenPropertyName.Length - 1];
            if (!char.IsDigit(childOrdinalSuffix))
                return relsToCurrParent[0];

            int fkIndex = int.Parse(childOrdinalSuffix.ToString()) - 1;
            return relsToCurrParent[fkIndex];
        }

        private static List<IComplexPropertyAdapter> CreateComplexPropertyAdaptersViaReflection(Type compositeEntityClass)
        {
            var complexProperties = GetComplexProperties(compositeEntityClass);
            var complexPropAdapters = new List<IComplexPropertyAdapter>();
            foreach (var complexProp in complexProperties)
                complexPropAdapters.Add(new ReflectionComplexPropertyAdapter(complexProp.Name));
            return complexPropAdapters;
        }

        private static IEnumerable<PropertyInfo> GetComplexProperties(Type compositeEntityClass)
        {
            foreach (PropertyInfo prop in compositeEntityClass.GetProperties())
            {
                foreach (Type implementedInterface in prop.PropertyType.GetInterfaces())
                {
                    if (implementedInterface == typeof(IDataObject))
                        yield return prop;
                }
            }
        }

        #endregion

        #region CopyFrom(IDataObjectDescriptor);

        /// <summary>Copies metadata from the provided descriptor into the current instance.</summary>
        /// <param name="descriptor">Descriptor whose properties are copied to the current instance.</param>
        public virtual void CopyFrom(IDataObjectDescriptor descriptor)
        {
            this.StatusPropertyName = descriptor.Fields.StatusPropertyName;

            this.DefaultPropertyName = descriptor.Fields.DefaultPropertyName;
            this.OrderByPropertyName = descriptor.Fields.OrderByPropertyName;

            this.CreatedByPropertyName = descriptor.Fields.CreatedByPropertyName;
            this.CreatedOnPropertyName = descriptor.Fields.CreatedOnPropertyName;
            this.UpdatedByPropertyName = descriptor.Fields.UpdatedByPropertyName;
            this.UpdatedOnPropertyName = descriptor.Fields.UpdatedOnPropertyName;

            this.OwnerIdPropertyName = descriptor.Fields.OwnerIdPropertyName;
            this.UnitIdPropertyName = descriptor.Fields.UnitIdPropertyName;
            this.TeamIdPropertyName = descriptor.Fields.TeamIdPropertyName;
            this.OrganizationIdPropertyName = descriptor.Fields.OrganizationIdPropertyName;

            CopyChangeTrackingPropertyNames(descriptor);
            CopyFields(descriptor);
        }

        private void CopyChangeTrackingPropertyNames(IDataObjectDescriptor descriptor)
        {
            if (descriptor.Fields.ChangeTrackingPropertyNames == null)
            {
                this.ChangeTrackingPropertyNames = null;
            }
            else
            {
                this.ChangeTrackingPropertyNames = new string[descriptor.Fields.ChangeTrackingPropertyNames.Length];
                descriptor.Fields.ChangeTrackingPropertyNames.CopyTo(this.ChangeTrackingPropertyNames, 0);
            }
        }

        private void CopyFields(IDataObjectDescriptor descriptor)
        {
            if (descriptor.FieldPermissions == null)
            {
                this.FieldPermissions = null;
            }
            else
            {
                List<FieldPermissionSet> fields = new List<FieldPermissionSet>();
                foreach (FieldPermissionSet fieldMeta in descriptor.FieldPermissions)
                    fields.Add(Clone(fieldMeta));

                this.FieldPermissions = fields.AsReadOnly();
            }
        }

        private static FieldPermissionSet Clone(FieldPermissionSet source)
        {
            FieldPermissionSet clone = new FieldPermissionSet(source.PropertyName)
            {
                CreatePermission = source.CreatePermission,
                ReadPermission = source.ReadPermission,
                UpdatePermission = source.UpdatePermission
            };

            return clone;
        }

        #endregion

        #region CopyFrom(IBusinessObjectDescriptor)

        /// <summary>Copies metadata from the provided descriptor into the current instance.</summary>
        /// <param name="descriptor">Descriptor whose properties are copied to the current instance.</param>
        public virtual void CopyFrom(IBusinessObjectDescriptor descriptor)
        {
            CopyFrom((IDataObjectDescriptor)descriptor);

            this.permissions = new EntityPermissionSet();
            this.permissions.ReadPermission = descriptor.Permissions.ReadPermission;
            this.permissions.ReadUnitPermission = descriptor.Permissions.ReadUnitPermission;
            this.permissions.ReadChildUnitPermission = descriptor.Permissions.ReadChildUnitPermission;
            this.permissions.ReadAllPermission = descriptor.Permissions.ReadAllPermission;
            this.permissions.ReadUnassignedPermission = descriptor.Permissions.ReadUnassignedPermission;

            this.permissions.CreatePermission = descriptor.Permissions.CreatePermission;
            this.permissions.UpdatePermission = descriptor.Permissions.UpdatePermission;
            this.permissions.DeletePermission = descriptor.Permissions.DeletePermission;

            this.permissions.AssignOwnerPermission = descriptor.Permissions.AssignOwnerPermission;
            this.permissions.AssignChildUnitPermission = descriptor.Permissions.AssignChildUnitPermission;
            this.permissions.AssignAnyUnitPermission = descriptor.Permissions.AssignAnyUnitPermission;
            this.permissions.AssignAnyTeamPermission = descriptor.Permissions.AssignAnyTeamPermission;

            this.permissions.ViewPkPermission = descriptor.Permissions.ViewPkPermission;
            this.permissions.UserInterfacePermission = descriptor.Permissions.UserInterfacePermission;

            CopyChildrenAdapters(descriptor);
            CopyComplexPropertiesAdapters(descriptor);
        }

        private void CopyComplexPropertiesAdapters(IBusinessObjectDescriptor descriptor)
        {
            if (descriptor.ComplexProperties == null)
            {
                this.ComplexProperties = null;
            }
            else
            {
                var complexPropertiesAdapters = new List<IComplexPropertyAdapter>();
                foreach (IComplexPropertyAdapter source in descriptor.ComplexProperties)
                    complexPropertiesAdapters.Add(source.Clone());

                this.ComplexProperties = complexPropertiesAdapters.AsReadOnly();
            }
        }

        private void CopyChildrenAdapters(IBusinessObjectDescriptor descriptor)
        {
            if (descriptor.Children == null)
            {
                this.Children = null;
            }
            else
            {
                var childrenAdapters = new List<IChildrenAdapter>();
                foreach (IChildrenAdapter source in descriptor.Children)
                    childrenAdapters.Add(source.Clone());

                this.Children = childrenAdapters.AsReadOnly();
            }
        }

        #endregion
    }
}